import { Point } from './ts-geometry-2d/Point';
import { TableAtrribute } from '../core/TableAttribute';
import { DeskBall, DeskHole, HitPath, AiGTool } from './aicore';
import { Vector } from './ts-geometry-2d/Vector';

// AI击球思考的更像人类
export class HitPathGenerate {
  // 球桌距离最大值的平方, 斜对角线的最大值
  public static MaxDistanceSquare = 2000;
  private radius = 0.5;
  // 生成所有的最好击球路径
  public generateAllBestHitPath(mainBall: DeskBall, attackBalles: Array<DeskBall>,
    protectBalles: Array<DeskBall>, deskHoles: Array<DeskHole>) {
    const paths = new Array<HitPath>();
    this.genMainTargetHole(mainBall, attackBalles, protectBalles, deskHoles, paths);
    this.genJointTargetHole(mainBall, attackBalles, protectBalles, deskHoles, paths);
    this.genMainTargetEdgeHole(mainBall, attackBalles, protectBalles, deskHoles, paths);
    if(paths.length <= 0) {
      this.genMainDefendTargetBall(mainBall, attackBalles, protectBalles, deskHoles, paths);
    }
    if (paths.length <= 0) {
      this.genMainEdgeTargetHole(mainBall, attackBalles, protectBalles, deskHoles, paths);
      this.genBackUp(mainBall, attackBalles, protectBalles, deskHoles, paths);
    }
    for (const path of paths) {
      if (path.targetBall && path.targetBall.score > 0) {
        path.score = path.score * path.targetBall.score;
      }
    }
    paths.sort((a, b) => b.score - a.score);
    return paths;
  }

  // 仅生成直接击球时最好击球路径【仅用于快速决策自由球时最好的位置】
  public generateOnlyDirectBestPath(mainBall: DeskBall, attackBalles: Array<DeskBall>,
    protectBalles: Array<DeskBall>, deskHoles: Array<DeskHole>) {
    const paths = new Array<HitPath>();
    this.genMainTargetHole(mainBall, attackBalles, protectBalles, deskHoles, paths);
    paths.sort((a, b) => b.score - a.score);
    return paths;
  }
  //【1.直接进洞 白球 -> 目标球 -> 球洞】 [10 无穷]
  private genMainTargetHole(mainBall: DeskBall, attackBalles: Array<DeskBall>,
    protectBalles: Array<DeskBall>, deskHoles: Array<DeskHole>, paths: Array<HitPath>) {
    for (const ball of attackBalles) {
      const balls = attackBalles.filter(b => b.index !== ball.index);
      // 排除自己以外的所有球，都可能形成遮挡
      const allBalls = balls.concat(protectBalles);
      const dirm = ball.position.minus(mainBall.position).normed();
      for (const hole of deskHoles) {
        // 目标球到球洞 有遮挡
        if (AiGTool.hasOccluded(ball.position, hole.position, allBalls)) { continue; }
        const dira = hole.position.minus(ball.position).normed();
        // 锐角才有进球角度
        if (dira.dot(dirm) < 0.05) { continue; }
        if (hole.isMiddleHole() && this.isNotInMiddleHitRange(ball.position)) {continue; }
        // 计算真实的击球点
        const rp = this.calcuRealPosition(ball.position, hole.position);
        // 白球到碰撞点有遮挡
        if (AiGTool.hasOccluded(mainBall.position, rp, allBalls)) { continue; }
        const path = new HitPath(HitPath.HitTypeDirect, rp, ball, hole);
        this.calcuMainTargetHoleScore(mainBall.position, rp, hole, path);
        const factor = this.mainBallHoledFactor(mainBall.position, rp, hole, deskHoles);
        path.setScore(path.score / factor);
        paths.push(path);
      }
    }
  }
  //【2. 球打球进球：白球 -> 自己球 -> 目标球 -> 球洞】 [5 10]
  private genJointTargetHole(mainBall: DeskBall, attackBalles: Array<DeskBall>,
    protectBalles: Array<DeskBall>, deskHoles: Array<DeskHole>, paths: Array<HitPath>) {
    // 目标球不能离洞口太远, 否则这种球打球的方式不容易进，也不用考虑
    const minThresSquare = (TableAtrribute.DeskHalfAxisZLength - 3) *  (TableAtrribute.DeskHalfAxisZLength - 3);
    for (var i=0; i<attackBalles.length; i++) {
      var minhole = null;
      for (const hole of deskHoles) {
        const dis = hole.position.distanceSquare(attackBalles[i].position);
        if (dis < minThresSquare) { minhole = hole;  break; }
      }
      if (!minhole) continue;

      const balls = attackBalles.filter(b => b.index !== attackBalles[i].index);
      const allBalls = balls.concat(protectBalles);
      if (AiGTool.hasOccluded(attackBalles[i].position, minhole.position, allBalls)) {
        continue;
      }
      // j 是母球击打的球， i 进洞球
      for (var j=0; j<attackBalles.length; j++) {
        if (i === j) continue;
        const leftBalls = allBalls.filter(b => b.index !== attackBalles[j].index);
        if (AiGTool.hasOccluded(attackBalles[i].position, attackBalles[j].position, leftBalls)) {
          continue;
        }
        if (AiGTool.hasOccluded(mainBall.position, attackBalles[j].position, leftBalls)) {
          continue;
        }
        const rpa = this.calcuRealPosition(attackBalles[i].position, minhole.position);
        const rpm = this.calcuRealPosition(attackBalles[j].position, rpa);
        const path = new HitPath(HitPath.HitTypeIndirectCollision, rpm, attackBalles[j], minhole);
        this.calcuMainTargetHoleScore(mainBall.position, rpm, minhole, path);
        // 因为有传递性，所以在直接进球的基础上给予惩罚
        path.setScore(path.score / 3);
        paths.push(path);
      }
    }
  }
  //【3. 反弹进球】白球 -> 目标球 -> 桌面反射 进球 [2, 5]
  private genMainTargetEdgeHole(mainBall: DeskBall, attackBalles: Array<DeskBall>,
    protectBalles: Array<DeskBall>, deskHoles: Array<DeskHole>, paths: Array<HitPath>) {
    for (const ball of attackBalles) {
      const balls = attackBalles.filter(b => b.index !== ball.index);
      // 排除自己以外的所有球，都可能形成遮挡
      const allBalls = balls.concat(protectBalles);
      // 主球到目标球 有遮挡
      if (AiGTool.hasOccluded(mainBall.position, ball.position, allBalls)) { continue; }
      const dirm = ball.position.minus(mainBall.position).normed();
      for (const hole of deskHoles) {
        // 白球 目标球 球洞 是否组成反弹的结构
        const dira = hole.position.minus(ball.position).normed();
        // 钝角才有反弹进球角度 且大于 120 度
        if (dira.dot(dirm) > -0.5) { continue; }
        // 在边上的碰撞点
        const cp = new Point(0, 0);
        if (hole.isUpMiddleHole()) {
          const disttz = (TableAtrribute.DeskHalfAxisZLength - ball.position.y);
          const disthz = (TableAtrribute.DeskHalfAxisZLength - hole.position.y);
          const ax = hole.position.x - ball.position.x;
          cp.x = ball.position.x + ax * (disttz / (disttz + disthz));
          cp.y = TableAtrribute.DeskHalfAxisZLength;
        }
        if (hole.isDownMiddleHole()) {
          const disttz = (TableAtrribute.DeskHalfAxisZLength + ball.position.y);
          const disthz = (TableAtrribute.DeskHalfAxisZLength + hole.position.y);
          const ax = hole.position.x - ball.position.x;
          cp.x = ball.position.x + ax * (disttz / (disttz + disthz));
          cp.y = -TableAtrribute.DeskHalfAxisZLength;
        }
        if (hole.isLeftBottomHole()) {
          const disttx = (TableAtrribute.DeskHalfAxisXLength - ball.position.x);
          const disthx = (TableAtrribute.DeskHalfAxisXLength - hole.position.x);
          const ay = hole.position.y - ball.position.y;
          cp.x = TableAtrribute.DeskHalfAxisXLength;
          cp.y = ball.position.x + ay * (disttx / (disttx + disthx));
        }
        if (hole.isRightBottomHole()) {
          const disttx = (TableAtrribute.DeskHalfAxisXLength + ball.position.x);
          const disthx = (TableAtrribute.DeskHalfAxisXLength + hole.position.x);
          const ay = hole.position.y - ball.position.y;
          cp.x = -TableAtrribute.DeskHalfAxisXLength;
          cp.y = ball.position.x + ay * (disttx / (disttx + disthx));
        }

        if (AiGTool.hasOccluded(ball.position, cp, allBalls)) { continue; }
        if (AiGTool.hasOccluded(cp, hole.position, allBalls)) { continue; }
        const rp = this.calcuRealPosition(ball.position, cp);

        const dirc = cp.minus(rp).normed();
        const hitjiao = dirc.dot(dirm);
         // 击球角度 小于45度才可以
        if (dirc.dot(dirm) < 0.7071) { continue; }

        const path = new HitPath(HitPath.HitTypeReboundHoled, rp, ball, hole);
        path.setScore(1 + hitjiao);
        // 中洞更容易反弹，给予一些加分
        if (hole.isMiddleHole()) { path.setScore(2 + 5 * hitjiao); }
        path.setForce(0.5, 0.4, 1.0);
        paths.push(path);
      }
    }
  }
  //【4. 防守方案】【可以打到球但是没有洞】【白球 -> 目标球】 【能碰到球即可】[1, 2]
  private genMainDefendTargetBall(mainBall: DeskBall, attackBalles: Array<DeskBall>,
    protectBalles: Array<DeskBall>, deskHoles: Array<DeskHole>, paths: Array<HitPath>) {
    // 根据球的位置计算分数值，将边角落的球解救出来
    for (const ball of attackBalles) {
      const balls = attackBalles.filter(b => b.index !== ball.index);
      const allBalls = balls.concat(protectBalles);
      const dir = mainBall.position.minus(ball.position).normed();
      const oball = new Point(ball.position.x, ball.position.y);
      if (!AiGTool.hasOccluded(mainBall.position, oball, allBalls)) {
        const path = new HitPath(HitPath.HitTypeReboundCollision, oball, ball, deskHoles[0]);
        path.setScore(this.calcuBallPosScore(ball.position));
        paths.push(path);
      }

      const ba = new Point(ball.position.x + dir.x * this.radius, ball.position.y + dir.y * this.radius);
      if (!AiGTool.hasOccluded(mainBall.position, ba, allBalls)) {
        const path = new HitPath(HitPath.HitTypeReboundCollision, ba, ball, deskHoles[0]);
        path.setScore(this.calcuBallPosScore(ball.position));
        paths.push(path);
      }

      // 垂直方向偏移
      const bal = new Point(ball.position.x - dir.y * this.radius, ball.position.y + dir.x * this.radius);
      if (!AiGTool.hasOccluded(mainBall.position, bal, allBalls)) {
        const path = new HitPath(HitPath.HitTypeReboundCollision, bal, ball, deskHoles[0]);
        path.setScore(this.calcuBallPosScore(ball.position));
        paths.push(path);
      }

      const bar = new Point(ball.position.x + dir.y * this.radius, ball.position.y - dir.x * this.radius);
      if (!AiGTool.hasOccluded(mainBall.position, bar, allBalls)) {
        const path = new HitPath(HitPath.HitTypeReboundCollision, bar, ball, deskHoles[0]);
        path.setScore(this.calcuBallPosScore(ball.position));
        paths.push(path);
      }
    }
    return paths;
  }
  //【5. 解球方案】【直接碰撞】【反弹碰撞：白球 -> 桌面反射 -> 目标球】 【能碰到球即可】[0, 1]
  private genMainEdgeTargetHole(mainBall: DeskBall, attackBalles: Array<DeskBall>,
    protectBalles: Array<DeskBall>, deskHoles: Array<DeskHole>, paths: Array<HitPath>) {
    // 反弹碰撞遍历4条桌边
    for (const ball of attackBalles) {
      const balls = attackBalles.filter(b => b.index !== ball.index);
      const allBalls = balls.concat(protectBalles);
      const rp1 = this.reflectByLine1(mainBall.position, ball.position);
      if (!this.reflectBallOccluded(mainBall, rp1, ball, allBalls)) {
        const path = new HitPath(HitPath.HitTypeReboundCollision, rp1, ball, deskHoles[0]);
        this.calcuMainReflectScore(mainBall.position, rp1, ball.position, path);
        paths.push(path);
      }
      const rp2 = this.reflectByLine2(mainBall.position, ball.position);
      if (!this.reflectBallOccluded(mainBall, rp2, ball, allBalls)) {
        const path = new HitPath(HitPath.HitTypeReboundCollision, rp2, ball, deskHoles[0]);
        this.calcuMainReflectScore(mainBall.position, rp2, ball.position, path);
        paths.push(path);
      }
      const rp3 = this.reflectByLine3(mainBall.position, ball.position);
      if (!this.reflectBallOccluded(mainBall, rp3, ball, allBalls)) {
        const path = new HitPath(HitPath.HitTypeReboundCollision, rp3, ball, deskHoles[0]);
        this.calcuMainReflectScore(mainBall.position, rp3, ball.position, path);
        paths.push(path);
      }
      const rp4 = this.reflectByLine4(mainBall.position, ball.position);
      if (!this.reflectBallOccluded(mainBall, rp4, ball, allBalls)) {
        const path = new HitPath(HitPath.HitTypeReboundCollision, rp4, ball, deskHoles[0]);
        this.calcuMainReflectScore(mainBall.position, rp4, ball.position, path);
        paths.push(path);
      }
    }
  }
  //【6. 兜底方案, 随机爆杆】随机打一杆 [0]
  private genBackUp(mainBall: DeskBall, attackBalles: Array<DeskBall>,
    protectBalles: Array<DeskBall>, deskHoles: Array<DeskHole>, paths: Array<HitPath>) {
    // 各种方案都没有的话，那么随机打一杆兜底
    if (paths.length !== 0) {
      return;
    }
    const rp = new Point(10 * (Math.random() - 0.5), 5 * (Math.random() - 0.5));
    const path = new HitPath(HitPath.HitTypeBackUp, rp, attackBalles[0], deskHoles[0]);
    // 兜底随机一杆，则使用最大力度，所谓大力出奇迹
    path.setForce(1.0, 1.0, 1.0);
    paths.push(path);
  }
  // 计算真正击球点的坐标
  private calcuRealPosition(targetPos: Point, holePos: Point) {
    const dir = targetPos.minus(holePos).normed();
    return new Point(targetPos.x + 2 * this.radius * dir.x, targetPos.y + 2 * this.radius * dir.y);
  }
  // 计算 白球 -> 目标球 -> 球洞 分值
  private calcuMainTargetHoleScore(mpos: Point, targetPos: Point, hole: DeskHole, path: HitPath) {
    var distmt = mpos.distanceSquare(targetPos);
    if (distmt > HitPathGenerate.MaxDistanceSquare) distmt = HitPathGenerate.MaxDistanceSquare;
    var distth = targetPos.distanceSquare(hole.position);
    if (distth > HitPathGenerate.MaxDistanceSquare) distth = HitPathGenerate.MaxDistanceSquare;
    
    const dira = targetPos.minus(mpos).normed();
    const dirb = hole.position.minus(targetPos).normed();
    const costheta = dira.dot(dirb); // [0, 1] [0度，90度]

    // 三个因素共同决定  球桌最大距离的平方 HitPathGenerate.MaxDistanceSquare
    // alpha: 主球到目标球距离因子
    // beta: 目标球到球洞距离因子
    // gama: 夹角因子
    const alpha = Math.sqrt(((HitPathGenerate.MaxDistanceSquare - distmt) / HitPathGenerate.MaxDistanceSquare));
    const beta = Math.sqrt((HitPathGenerate.MaxDistanceSquare - distth) / HitPathGenerate.MaxDistanceSquare);
    const gama = costheta;

    const fa = Math.sqrt(1 - alpha);
    const fb = Math.sqrt(1 - beta);

    path.setScore(Math.exp(alpha) * Math.exp(4 * beta) * Math.exp(4 * gama));
    const {force, minForce, maxForce} = this.calcuForce(costheta, fa, fb);
    path.setForce(force, minForce, maxForce);
    if (hole.isMiddleHole()) { // 中洞，主要看方向
      // 中洞击球角度，主要用于缩小分值，不能超过 1，角度不够，中洞最好不要击球
      path.setForce(0.5 * force , 0.3 * minForce, maxForce);
    }
  }
  // 计算 白球 -> 桌边 -> 目标球  分值
  private calcuMainReflectScore(mpos: Point, edgePos: Point, targetBallPos: Point, path: HitPath) {
    const distmt = mpos.distanceSquare(edgePos);
    const distth = edgePos.distanceSquare(targetBallPos);
    const dira = mpos.minus(edgePos).normed();
    const dirb = targetBallPos.minus(edgePos).normed();
    const costheta = 1 - Math.abs(dira.dot(dirb)); // [-1, 1] [0度，180度]  90度最好

    // 三个因素共同决定
    const alpha = Math.exp((2000 - distmt) / 2000) / Math.E;  // 球桌最大距离的平方 2000
    const beta = Math.exp((2000 - distth) / 2000) / Math.E;
    const gama = costheta;

    path.setScore(alpha * Math.exp(2.5 * beta) * Math.exp(0.5 * gama));
    path.setForce((alpha + beta) / 2.2, (alpha + beta) / 3, (alpha + beta));
  }
  // calcuBallPosScore 计算球的位置复杂分数值
  private calcuBallPosScore(ballPos: Point) {
    const fx = 1 - (TableAtrribute.DeskHalfAxisXLength - Math.abs(ballPos.x)) / TableAtrribute.DeskHalfAxisXLength;
    const fy = 1 - (TableAtrribute.DeskHalfAxisZLength - Math.abs(ballPos.y)) / TableAtrribute.DeskHalfAxisZLength;
    return 2 * fx * fy + (fx + fy) / 2;
  }
  private calcuDistanceScore(mpos: Point, to: Point, path: HitPath) {
    var distmt = mpos.distanceSquare(to);
    if (distmt > HitPathGenerate.MaxDistanceSquare)  distmt = HitPathGenerate.MaxDistanceSquare;
    const alpha = Math.sqrt(((HitPathGenerate.MaxDistanceSquare - distmt) / HitPathGenerate.MaxDistanceSquare));
    
    var distbet = to.distanceSquare(path.targetHole.position);
    if (distbet > HitPathGenerate.MaxDistanceSquare) distbet = HitPathGenerate.MaxDistanceSquare;
    const beta = Math.sqrt(((HitPathGenerate.MaxDistanceSquare - distbet) / HitPathGenerate.MaxDistanceSquare));
    
    path.setScore(alpha * beta);
    path.setForce(10 * Math.sqrt(1 - alpha), 0.1, 1.0);
  }

  private reflectBallOccluded(mainBall: DeskBall, rp: Point, ball: DeskBall, allBalls: Array<DeskBall>) {
    // 主球到桌边
    if (AiGTool.hasOccluded(mainBall.position, rp, allBalls)) {
      return true;
    }
    // 桌边 到 目标球
    if (AiGTool.hasOccluded(rp, ball.position, allBalls)) {
      return true;
    }
    return false;
  }
  private reflectByLine1(mpos: Point, bpos: Point) {
    // 边1 y = -DeskHalfAxisZLength
    const distm = Math.abs(mpos.y + TableAtrribute.DeskHalfAxisZLength);
    const distb = Math.abs(bpos.y + TableAtrribute.DeskHalfAxisZLength);
    const targetx = mpos.x - (mpos.x - bpos.x) * distm / (distm + distb);
    const rp = new Point(targetx, -TableAtrribute.DeskHalfAxisZLength);
    return rp;
  }

  private reflectByLine2(mpos: Point, bpos: Point) {
    // 边2 x = DeskHalfAxisXLength
    const distm = Math.abs(mpos.x - TableAtrribute.DeskHalfAxisXLength);
    const distb = Math.abs(bpos.x - TableAtrribute.DeskHalfAxisXLength);
    const targety = mpos.y - (mpos.y - bpos.y) * distm / (distm + distb);
    const rp = new Point(TableAtrribute.DeskHalfAxisXLength, targety);
    return rp;
  }

  private reflectByLine3(mpos: Point, bpos: Point) {
    // 边1 y = DeskHalfAxisZLength
    const distm = Math.abs(mpos.y - TableAtrribute.DeskHalfAxisZLength);
    const distb = Math.abs(bpos.y - TableAtrribute.DeskHalfAxisZLength);
    const targetx = mpos.x - (mpos.x - bpos.x) * distm / (distm + distb);
    const rp = new Point(targetx, TableAtrribute.DeskHalfAxisZLength);
    return rp;
  }

  private reflectByLine4(mpos: Point, bpos: Point) {
    // 边1 x = -DeskHalfAxisXLength
    const distm = Math.abs(mpos.x + TableAtrribute.DeskHalfAxisXLength);
    const distb = Math.abs(bpos.x + TableAtrribute.DeskHalfAxisXLength);
    const targety = mpos.y - (mpos.y - bpos.y) * distm / (distm + distb);
    const rp = new Point(-TableAtrribute.DeskHalfAxisXLength, targety);
    return rp;
  }
  // 计算击球力度，force，minForce, maxForce
  private calcuForce(gama: number, alpha: number, beta: number) {
    // gama cos夹角，alpha 白球到目标球，beta 目标球到球洞
    const force = 0.8 * beta / ((1 - alpha) * gama) + 0.2 * (alpha + beta) * (1 - gama);
    const minForce = 0.4 * beta / ((1 - alpha) * gama) + 0.1 * (alpha + beta) * (1 - gama);
    const maxForce = 1.0;
    return {force, minForce, maxForce}
  }
  // 白球进洞违规的罚分【1, 10000】
  private mainBallHoledFactor(mpos: Point, targetPos: Point, hole: DeskHole, deskHoles: Array<DeskHole>) {
    const dira = hole.position.minus(targetPos).normed();
    const dirv = targetPos.minus(mpos).normed();
    const dirb = this.calcuVectorDecompose(dirv, dira);

    // 目标球位置 到 球洞位置
    for (const dh of deskHoles) {
      const dist = Math.sqrt((dh.position.x - targetPos.x) * (dh.position.x - targetPos.x) + (dh.position.y - targetPos.y) * (dh.position.y - targetPos.y));
      if (dist > 3.5) { continue; }
      const thres = this.radius / dist; // 夹角的cos约等于
      const dirm = dh.position.minus(targetPos).normed();
      const costh = dirm.x * dirb.x + dirm.y * dirb.y;
      if (costh < thres) { continue; }
      if (costh >= 0.99) {
        return 10000;
      }
      // costh > thres > 0
      return 100 / ((1 - costh) * dist);
    }
    return 1;
  }
  private isHitProtectBall(rp: Point, ballPos: Point, protectBalles: Array<DeskBall>) {
    for (const pb of protectBalles) {
      const dist = Math.sqrt((pb.position.x - rp.x) * (pb.position.x - rp.x) + (pb.position.y - rp.y) * (pb.position.y - rp.y));
      if (dist < 2*this.radius) {
        return true;
      }
    }
    return false;
  }
  // v向量分解成a，b向量，已知v和a，求解b向量, v, a, b都是单元化的向量
  private calcuVectorDecompose(dirv: Vector, dira: Vector) {
    // dirv = lambda * dira + niu * dirb
    const lambda = dirv.x * dira.x + dirv.y * dira.y;
    if (Math.abs(lambda) < 0.01) {
      return new Vector(dirv.x, dirv.y, dirv.w);
    }
    var dirb = new Vector(dirv.x - lambda * dira.x, dirv.y - lambda * dira.y, 1);
    return dirb.normed();
  }
  private isNotInMiddleHitRange(ball: Point) {
    const dists = ball.x * ball.x + ball.y * ball.y;
    if (dists > TableAtrribute.DeskHalfAxisZLength * TableAtrribute.DeskHalfAxisZLength) {
      return true;
    }
    if (ball.x + ball.y - TableAtrribute.DeskHalfAxisZLength > 0) { return true; }
    if (ball.x - ball.y - TableAtrribute.DeskHalfAxisZLength > 0) { return true; }

    if (ball.x - ball.y + TableAtrribute.DeskHalfAxisZLength < 0) { return true; }
    if (ball.x + ball.y + TableAtrribute.DeskHalfAxisZLength < 0) { return true; }
    return false;
  }
}

